// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt.webinf;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.jsp.tagext.BodyTag;
import javax.servlet.jsp.tagext.IterationTag;
import javax.servlet.jsp.tagext.Tag;
import javax.servlet.jsp.tagext.TagAttributeInfo;
import javax.servlet.jsp.tagext.TagExtraInfo;
import javax.servlet.jsp.tagext.TagInfo;
import javax.servlet.jsp.tagext.TryCatchFinally;
import org.owasp.jxt.JxtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TagDefinition
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
public final class TagDefinition {
    private static final Logger _log = LoggerFactory.getLogger(TagDefinition.class);

    private String _name;
    private String _tagClass;
    private String _teiClass;
    private BodyContentType _bodyContent = BodyContentType.JSP;
    private String _infoString;

    private List<AttributeDefinition> _attributes = new ArrayList<AttributeDefinition>();
    private Map<String,AttributeDefinition> _attributeMap = new HashMap<String,AttributeDefinition>();

    private TagInfo _tagInfo;
    private TagExtraInfo _tagExtraInfo;

    private boolean _tryCatchFinally;
    private boolean _iterationTag;
    private boolean _bodyTag;

    static Class<?> findSetterType(Method[] methods, String setter) {
        for (Method m : methods) {
            if (setter.equals(m.getName()) &&
                m.getReturnType() == Void.TYPE &&
                m.getParameterTypes().length == 1)
            {

                // there doesn't seem to be any provision in the JSP
                // spec for an attribute with multiple types.  We just
                // pick out the first matching setter and use that.

                return m.getParameterTypes()[0];
            }
        }

        return null;
    }

    TagAttributeInfo[] loadAttributeInfo(
        Class<? extends Tag> tagImpl,
        List<String> errors)
    {
        final Method[] methods = tagImpl.getMethods();

        List<TagAttributeInfo> attributeInfos = new ArrayList<TagAttributeInfo>();

        for (AttributeDefinition attr : _attributes) {
            String setter = JxtUtils.setterName(attr.getName());

            Class<?> type = findSetterType(methods, setter);

            if (type == null) {
                errors.add("Could not find setter for attribute '"+attr.getName()+"' on "+tagImpl.getName());
                continue;
            }

            attr.setType(type);

            TagAttributeInfo attrInfo = new TagAttributeInfo(
                attr.getName(), attr.isRequired(), type.getName(), attr.isRtExprValue());

            attributeInfos.add(attrInfo);
        }

        return attributeInfos.toArray(new TagAttributeInfo[0]);
    }

    TagExtraInfo loadTeiInstance(List<String> errors) {
        if (_teiClass != null) {
            try {
                Class<? extends TagExtraInfo> teiImpl = Class.forName(_teiClass).asSubclass(TagExtraInfo.class);
                TagExtraInfo tei = teiImpl.newInstance();
                _log.debug("Loaded: {}", tei);
                return tei;
            } catch (ClassNotFoundException e) {
                errors.add("Could not find "+_teiClass);
            } catch (ClassCastException e) {
                errors.add(_teiClass+" does not extend "+TagExtraInfo.class.getName());
            } catch (InstantiationException e) {
                errors.add("Could not instantiate "+_teiClass+": "+e);
            } catch (IllegalAccessException e) {
                errors.add("Could not access "+_teiClass+": "+e);
            }
        }

        return null;
    }

    public void load(TagLibraryInfoImpl tld, List<String> errors) {
        if (_tagClass == null) {
            throw new IllegalArgumentException("No tagclass defined for "+_name);
        }

        Class<? extends Tag> tagImpl;

        try {
            tagImpl = Class.forName(_tagClass).asSubclass(Tag.class);
        } catch (ClassNotFoundException e) {
            errors.add("Class "+_tagClass+" not found");
            return;
        } catch (ClassCastException e) {
            errors.add(_tagClass+" is not an instance of "+Tag.class.getName());
            return;
        }

        // TODO: also check that tagImpl has a public default
        // constructor

        TagAttributeInfo[] attrInfoArray = loadAttributeInfo(tagImpl, errors);

        if (true) { // JSP 1.1 format for TLD
            _tagInfo = new TagInfo(_name, _tagClass, _bodyContent.toString(), _infoString,
                                   tld, _tagExtraInfo, attrInfoArray);
        } else {
            // javax.servlet.jsp.tagext.TagVariableInfo[] tvi;

            String displayName = null;
            String smallIcon = null;
            String largeIcon = null;

            if (true) { // JSP 1.2 format for TLD
                _tagInfo = new TagInfo(_name, _tagClass, _bodyContent.toString(), _infoString,
                                       tld, _tagExtraInfo, attrInfoArray,
                                       displayName, smallIcon, largeIcon, null /* tvi */);
            } else { // JSP 2.0 format for TLD

                boolean dynamicAttributes = false;

                _tagInfo = new TagInfo(_name, _tagClass, _bodyContent.toString(), _infoString,
                                       tld, _tagExtraInfo, attrInfoArray,
                                       displayName, smallIcon, largeIcon, null /* tvi */,
                                       dynamicAttributes);
            }
        }

        _tagExtraInfo = loadTeiInstance(errors);

        if (_tagExtraInfo != null) {
            _tagExtraInfo.setTagInfo(_tagInfo);
        }

        _tryCatchFinally = TryCatchFinally.class.isAssignableFrom(tagImpl);
        _iterationTag = IterationTag.class.isAssignableFrom(tagImpl);
        _bodyTag = BodyTag.class.isAssignableFrom(tagImpl);
    }

    public void addAttribute(AttributeDefinition attr) {
        _attributes.add(attr);
        _attributeMap.put(attr.getName(), attr);
    }

    public AttributeDefinition getAttribute(String name) {
        return _attributeMap.get(name);
    }

    public Collection<AttributeDefinition> getAttributes() {
        return _attributes;
    }

    public void setName(String arg) { _name = arg; }
    public String getName() { return _name; }
    public void setTagClass(String arg) { _tagClass = arg; }
    public String getTagClass() { return _tagClass; }
    public void setTeiClass(String arg) { _teiClass = arg; }
    public String getTeiClass() { return _teiClass; }

    public TagExtraInfo getTagExtraInfo() {
        return _tagExtraInfo;
    }

    public void setBodyContent(BodyContentType arg) { _bodyContent = arg; }
    public BodyContentType getBodyContent() { return _bodyContent; }
    public void setInfoString(String arg) { _infoString = arg; }
    public String getInfoString() { return _infoString; }

    public boolean isTryCatchFinally() {
        return _tryCatchFinally;
    }

    public boolean isEmpty() {
        return _bodyContent == BodyContentType.empty;
    }

    public boolean isIterationTag() {
        return _iterationTag;
    }

    public boolean isBodyTag() {
        return _bodyTag;
    }

    public String toString() {
        return "{name:"+_name+", tagClass:"+_tagClass+
            ", teiClass:"+_teiClass+", bodyContent:"+_bodyContent+
            ", attributes:"+_attributes+"}";
    }

} // TagDefinition
